/**
 * This file is part of Lineage II Client Data Parser.
 * 
 * Lineage II Client Data Parser is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.

 * Lineage II Client Data Parser is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.

 * You should have received a copy of the GNU General Public License
 * along with Lineage II Client Data Parser.  If not, see <http://www.gnu.org/licenses/>.
 */
package net.l2emuproject.parser;

import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;

import net.l2emuproject.Launcher;
import net.l2emuproject.entity.MonsterSkill;
import net.l2emuproject.entity.MonsterSkillPassive;
import net.l2emuproject.io.FileManager;
import net.l2emuproject.io.SafelyPackedFile;
import net.l2emuproject.parser.base.DefaultParser;
import net.l2emuproject.redist.UnsupportedException;

import javolution.text.TextBuilder;

/**
 * @author Savormix
 *
 */
public class MonsterSkillParser extends DefaultParser
{
	protected static final String[]			NEEDED_FILES	=
															{ Launcher.FILES[17], Launcher.FILES[19] };
	private final List<MonsterSkill>		mSkillsA;
	private final List<MonsterSkillPassive>	mSkillsP;

	/**
	 * Creates a parser that parses monster skill data. Needed Files:
	 * <LI>MobSkillAnimgrp</LI>
	 * <LI>npcgrp</LI>
	 * @param spf File array
	 * @throws IllegalArgumentException Wrong file(s) 
	 */
	public MonsterSkillParser(SafelyPackedFile... spf) throws IllegalArgumentException
	{
		super(spf);
		mSkillsA = new ArrayList<MonsterSkill>(9000);
		mSkillsP = new ArrayList<MonsterSkillPassive>(10000);
	}

	/** Creates a parser that validates files */
	public MonsterSkillParser()
	{
		super();
		mSkillsA = null;
		mSkillsP = null;
	}

	/**
	 * Experimental analysis can use this to obtain either just parsed or both parsed and
	 * analyzed data from this parser and use it to check/correct/update/enhance precision
	 * of some other parsed data.
	 * @return Active skill list, not 100% accurate and might contain duplicates
	 */
	public List<MonsterSkill> getAltSkillList()
	{
		return mSkillsA;
	}

	/**
	 * Experimental analysis can use this to obtain either just parsed or both parsed and
	 * analyzed data from this parser and use it to check/correct/update/enhance precision
	 * of some other parsed data.
	 * @return mostly passive skill list, 100% accurate
	 */
	public List<MonsterSkillPassive> getSkillList()
	{
		return mSkillsP;
	}

	private boolean parseMobSkillAnimGrp(int entryCnt, SafelyPackedFile spf)
	{
		if (!isValidator())
			log.info("Reading " + entryCnt + " entries from " + spf.getFileName());
		int npc, sk;
		String seq, skill, name, clas;
		for (int i = 0; i < entryCnt; i++)
		{
			npc = spf.readInt();
			sk = spf.readInt();
			if (isValidator())
				return npc > 0 && sk > 0;
			log.debug("Reading NPC: " + npc + " skill " + sk);
			seq = spf.readPreciseString(spf.readInt());
			skill = spf.readString();
			name = spf.readString();
			int fake = spf.readUnsignedByte();
			if (fake < 6)
			{
				clas = spf.readString(fake);
			}
			else
			{
				spf.movePointer(2);
				int indicator = spf.readByte();
				log.debug("Indicator is " + indicator);
				spf.movePointer(-3);
				if (indicator == 0)
					clas = spf.readUTF16();
				else
					clas = spf.readString(fake);
			}
			mSkillsA.add(new MonsterSkill(npc, sk, seq, skill, name, clas));
		}
		log.info("Finished reading " + spf.getFileName());
		return false;
	}

	/* This is a limited implementation, used ONLY to extract skills */
	private boolean parseNpcGrp(int entryCnt, SafelyPackedFile spf)
	{
		if (!isValidator())
			log.info("Reading " + entryCnt + " entries from " + spf.getFileName());
		int npc, skID, skLvl, cnt;
		for (int i = 0; i < entryCnt; i++)
		{
			npc = spf.readInt();
			if (isValidator())
				return npc > 0;
			// Class
			spf.readPreciseString(spf.readInt());
			// mesh
			spf.readPreciseString(spf.readInt());
			// text1 count
			cnt = spf.readInt();
			for (int j = 0; j < cnt; j++)
				spf.readPreciseString(spf.readInt());
			// text2 count
			cnt = spf.readInt();
			for (int j = 0; j < cnt; j++)
				spf.readPreciseString(spf.readInt());
			// dtab1 count
			cnt = spf.readByte();
			if (cnt == 1) // the next 4 bytes will always be 0x00 
				spf.readInt();
			for (int j = 0; j < (cnt / 2); j++)
			{
				skID = spf.readInt();
				skLvl = spf.readInt();
				mSkillsP.add(new MonsterSkillPassive(npc, skID, skLvl));
			}
			// speed
			spf.readFloat();
			// unk0 count
			cnt = spf.readInt();
			for (int j = 0; j < cnt; j++)
				spf.readPreciseString(spf.readInt());
			// snd1 count
			cnt = spf.readInt();
			for (int j = 0; j < cnt; j++)
				spf.readPreciseString(spf.readInt());
			// snd2 count
			cnt = spf.readInt();
			for (int j = 0; j < cnt; j++)
				spf.readPreciseString(spf.readInt());
			// snd3 count
			cnt = spf.readInt();
			for (int j = 0; j < cnt; j++)
				spf.readPreciseString(spf.readInt());
			// is raid
			if (spf.readInt() == 1)
				spf.readPreciseString(spf.readInt());
			// raid boss effect fl
			spf.readInt();
			// unk1 count
			cnt = spf.readByte();
			for (int j = 0; j < cnt; j++)
				spf.readPreciseString(spf.readInt());
			setPointerToEffect(spf);
			//effect
			spf.readPreciseString(spf.readInt());
			// unk2
			spf.readInt();
			// sound radius
			spf.readFloat();
			// sound volume
			spf.readFloat();
			// sound rnd
			spf.readFloat();
			// quest monster?
			spf.readInt();
			// class lim
			spf.readInt();
		}
		log.info("Finished reading " + spf.getFileName());
		return false;
	}

	/** (documented to clarify what it does)<BR>
	 * First of all, skip first four bytes, since it SHOULD contain effect length.<BR>
	 * Then, we either have a random number of random integers (4-byte, we skip them all)
	 * until we have (char ID, 0x00, charID, 0x00). That indicates the pointer is now
	 * past the beginning of the String.<BR>
	 * So we move back 4 bytes to the start of the String and then move back 4 bytes again
	 * so that we could read the string length. 
	 * @param spf the file contents
	 */
	protected void setPointerToEffect(SafelyPackedFile spf)
	{
		spf.readInt();
		int test1 = 0;
		int test2 = 0;
		while (test1 == 0 || test2 == 0)
		{
			test1 = spf.readUnsignedShort();
			test2 = spf.readUnsignedShort();
		}
		spf.movePointer(-8);
	}

	/* (non-Javadoc)
	 * @see com.l2cdp.parser.IParser#getNeededFiles()
	 */
	@Override
	public String[] getNeededFiles()
	{
		return NEEDED_FILES;
	}

	/* (non-Javadoc)
	 * @see com.l2cdp.parser.IParser#getParseType()
	 */
	@Override
	public String getParseType()
	{
		return "monster skill";
	}

	/* (non-Javadoc)
	 * @see com.l2cdp.parser.IParser#readClientData()
	 */
	@Override
	public void readClientData() throws IllegalAccessException
	{
		if (isValidator())
			throw new IllegalAccessException(toString());
		parseMobSkillAnimGrp(entries[order[0]], spf[order[0]]);
		parseNpcGrp(entries[order[1]], spf[order[1]]);
	}

	/* (non-Javadoc)
	 * @see com.l2cdp.parser.IParser#selfAdvancedAnalysis()
	 */
	@Override
	public void selfAdvancedAnalysis() throws IllegalAccessException
	{
		if (isValidator())
			throw new IllegalAccessException(toString());
		for (MonsterSkill ms : mSkillsA)
		{
			if (ANALYZE_DETERMINISTIC)
				ms.deterministicAnalysis();
			if (ANALYZE_EXPERIMENTAL)
				ms.experimentalAnalysis();
		}
		for (MonsterSkillPassive msp : mSkillsP)
		{
			if (ANALYZE_DETERMINISTIC)
				msp.deterministicAnalysis();
			if (ANALYZE_EXPERIMENTAL)
				msp.experimentalAnalysis();
		}
	}

	/* (non-Javadoc)
	 * @see com.l2cdp.parser.IParser#validate(com.l2cdp.entity.SafelyPackedFile)
	 */
	@Override
	public boolean validate(SafelyPackedFile spf)
	{
		try
		{
			if (spf.getFileName().equalsIgnoreCase(NEEDED_FILES[0]))
				return parseMobSkillAnimGrp(spf.readInt(), spf);
			else if (spf.getFileName().equalsIgnoreCase(NEEDED_FILES[1]))
				return parseNpcGrp(spf.readInt(), spf);
			else
				return false;
		}
		catch (Exception e)
		{
			return false;
		}
	}

	/* (non-Javadoc)
	 * @see com.l2cdp.parser.IParser#writeServerData(int)
	 */
	@Override
	public void writeServerData(int chronicle) throws IllegalAccessException
	{
		if (isValidator())
			throw new IllegalAccessException(toString());
		synchronized (finLock)
		{
			addThread();
		}
		new Thread(new AsyncWriterMSSrv(chronicle), AsyncWriterMSSrv.class.getName()).start();
	}

	/* (non-Javadoc)
	 * @see com.l2cdp.parser.IParser#writeSimpleData(int)
	 */
	@Override
	public void writeSimpleData(int chronicle) throws IllegalAccessException
	{
		if (isValidator())
			throw new IllegalAccessException(toString());
		synchronized (finLock)
		{
			addThread();
		}
		new Thread(new AsyncWriterMSSim(chronicle), AsyncWriterMSSim.class.getName()).start();
	}

	protected class AsyncWriterMSSim implements Runnable
	{
		private final int	cID;

		protected AsyncWriterMSSim(int cID)
		{
			this.cID = cID;
		}

		/* (non-Javadoc)
		 * @see java.lang.Runnable#run()
		 */
		@Override
		public void run()
		{
			try
			{
				Writer w = FileManager.getWriter(cID, "npcskills.properties", true);
				TextBuilder sb = TextBuilder.newInstance();
				addGenInfo(cID, sb, COMMENT_PROP);
				sb.append("# Skills below are displayed in client (below hp)");
				sb.append(LINE_BREAK);
				for (MonsterSkillPassive msp : mSkillsP)
				{
					sb.append(msp.getNpcID());
					sb.append(EQUAL);
					sb.append(msp.getSkillID());
					sb.append(COMMA);
					sb.append(msp.getSkillLevel());
					sb.append(LINE_BREAK);
				}
				sb.append(LINE_BREAK);
				sb.append("# Skills below are 100% Active & used by monsters");
				sb.append(LINE_BREAK);
				for (MonsterSkill ms : mSkillsA)
				{
					sb.append(ms.getNpcID());
					sb.append(EQUAL);
					sb.append(ms.getSkillID());
					sb.append(COMMA);
					sb.append(ms.getSkillLevel());
					sb.append(SEMICOLON);
					sb.append(ms.getSeqName());
					sb.append(SEMICOLON);
					sb.append(ms.getSkillName());
					sb.append(SEMICOLON);
					sb.append(ms.getNpcName());
					sb.append(SEMICOLON);
					sb.append(ms.getNpcClass());
					sb.append(LINE_BREAK);
				}
				addGenInfo(-1, sb, COMMENT_PROP);
				w.write(sb.toString());
				w.close();
			}
			catch (UnsupportedException e)
			{
				log.fatal("Unsupported chronicle, will not write data!", e);
			}
			catch (IOException e)
			{
				log.error("Cannot write npcskills.properties!", e);
			}
			finally
			{
				synchronized (finLock)
				{
					remThread();
				}
			}
		}
	}

	protected class AsyncWriterMSSrv implements Runnable
	{
		private final int	cID;

		protected AsyncWriterMSSrv(int cID)
		{
			this.cID = cID;
		}

		/* (non-Javadoc)
		 * @see java.lang.Runnable#run()
		 */
		@Override
		public void run()
		{
			try
			{
				Writer w = FileManager.getWriter(cID, "npcskills.sql", false);
				TextBuilder sb = new TextBuilder(10000);
				addGenInfo(cID, sb, COMMENT_SQL);
				// clear the biased table
				sb.append("TRUNCATE TABLE `npcskills`;");
				sb.append(LINE_BREAK);
				sb.append(LINE_BREAK);
				sb.append("-- Skills shown in client; 100% accuracy");
				sb.append(LINE_BREAK);
				sb.append("INSERT INTO `npcskills` VALUES");
				sb.append(LINE_BREAK);
				for (MonsterSkillPassive msp : mSkillsP)
				{
					sb.append(PAREN_OPEN);
					sqlDeclare(sb, msp.getNpcID());
					sqlDeclare(sb, msp.getSkillID());
					sqlDeclare(sb, msp.getSkillLevel(), true);
				}
				sb.delete(sb.length() - 3, sb.length());
				sb.append(SEMICOLON);
				sb.append(LINE_BREAK);
				sb.append(LINE_BREAK);
				sb.append("-- Skills cast by monsters; Level is 95% accurate");
				sb.append(LINE_BREAK);
				sb.append("INSERT IGNORE INTO `npcskills` VALUES");
				sb.append(LINE_BREAK);
				for (MonsterSkill ms : mSkillsA)
				{
					sb.append(PAREN_OPEN);
					sqlDeclare(sb, ms.getNpcID());
					sqlDeclare(sb, ms.getSkillID());
					sqlDeclare(sb, ms.getSkillLevel(), true);
				}
				sqlFinishQuery(sb);
				w.write(sb.toString());
				w.close();
			}
			catch (UnsupportedException e)
			{
				log.fatal("Unsupported chronicle, will not write data!", e);
			}
			catch (IOException e)
			{
				log.error("Cannot write npcskills.sql!", e);
			}
			finally
			{
				synchronized (finLock)
				{
					remThread();
				}
			}
		}
	}
}
